🏗️ Spring 계층 구조 정리
1️⃣ 요청 흐름 개요
Spring 기반 애플리케이션에서 데이터가 이동하는 일반적인 흐름은 다음과 같습니다.
Client → Controller → DTO → Service → Entity → DAO/Repository → DB
↕
Response DTO
- Client: 브라우저, 모바일 앱 등
- Controller: 요청 수신, DTO 변환, Service 호출, 응답 반환
- DTO: 요청/응답 데이터 캡슐화
- Service: 비지니스 로직 처리, Entity 변환, DAO 호출
- Entity: DB 테이블과 매핑되는 객체, 상태와 날짜 관리
- DAO/Repository: DB 접근, Entity CRUD 수행
- DB: 실제 데이터 저장 및 조회
💡 이 구조를 따르면 각 계층 책임이 명확해지고, 유지보수가 쉬워집니다.
2️⃣ Controller: 요청의 시작점
Controller는 클라이언트 요청을 받고, Servie에 전달하며, 결과를 다시 클라이언트에게 반환하는 최초 접점입니다.
🛠️ 역할
- 요청 URL 매핑 (
@RequestMapping,@PostMapping등) - HTTP 요청 데이터 바인딩 (
@RequestBody,@RequestParam) - DTO 생성/검증 (단순 수준)
- Service 호출
- Response DTO 반환
@RestController
@RequestMapping("/api/reservation")
public class ReservationController {
@Autowired
private ReservationService reservationService;
@PostMapping
public Result<ReservationResponseDto> createReservation(@RequestBody ReservationDto requestDto) {
// Service 호출
return reservationService.createReservation(requestDto);
}
}
✅ Controller는 로직을 직접 처리하지 않고, 요청과 응답을 연결하는 역할만 수행
3️⃣ DTO: 데이터 캡슐화
DTO(Data Transfer Object)는 Controller와 Service 사이에서 데이터를 전달용으로 캡슐화합니다.
Entity를 그래도 쓰면 DB 구조가 외부 API에 노출될 수 있고, 불필요한 필드까지 전달될 수 있습니다.
🛠️ 역할
- 요청 데이터 구조 저의
- 응답 데이터 구조 정의
- Validation 수행 가능 (필수값, 형식 등)
- Entity와 1:1 매핑 불필요
public class ReservationDto {
private String customerName;
private String customerPhone;
private String regionSido;
private DesiredOpeningType desiredOpeningType;
private Date scheduleAt;
// getter / setter
}
public class ReservationResponseDto extends ReservationDto {
private Long id;
private Date createdAt;
private Status status;
private String errorMessage;
// getter / setter
}
💡 DTO를 사용하면 API 변경에 따른 DB 영향 최소화가 가능
4️⃣ Service: 비지니스 로직 중심
Service는 애플리케이션의 핵심 로직을 수행하는 계층입니다.
🛠️ 역할
- 요청 DTO 검증
- Entity 변환 (DTO → Entity)
- DB CRUD 수행 (DAO 호출)
- 결과 DTO 생성 (Entity → Response DTO)
- 트랜잭션 관리 (
@Transactional)
@Service
public class ReservationService {
@Autowired
private ReservationDao reservationDao;
public Result<ReservationResponseDto> createReservation(ReservationDto dto) {
// 1️⃣ Validation
if (dto.getCustomerName() == null) {
ReservationResponseDto error = new ReservationResponseDto();
error.setErrorMessage("예약자 이름은 필수 입력사항입니다.");
return Result.error(error);
}
// 2️⃣ DTO → Entity 변환
Reservation reservation = new Reservation();
reservation.setToken(UUID.randomUUID().toString());
reservation.setCustomerName(dto.getCustomerName());
reservation.setRegionSido(dto.getRegionSido());
reservation.setDesiredOpeningType(dto.getDesiredOpeningType());
reservation.setScheduleAt(dto.getScheduleAt());
// 3️⃣ DB 저장
reservationDao.insert(reservation);
// 4️⃣ Entity → Response DTO 변환
ReservationResponseDto response = new ReservationResponseDto();
response.setId(reservation.getId());
response.setCustomerName(reservation.getCustomerName());
response.setCreatedAt(reservation.getCreatedAt());
response.setStatus(reservation.getStatus());
return Result.success(response);
}
}
✅ Service는 로직 처리의 핵심
❌ Controller에서 직접 DB 접근이나 비즈니스 로직을 처리하면 책임이 혼재됨
Entity: DB와 매핑되는 도메인 객체
Entity는 DB 테이블과 1:1 매핑되는 객체입니다.
🛠️ 역할
- DB 컬럼 정의
- Enum, Temporal 타입 매핑
- 생성/수정일 자동 관리 (
@PrePersist,@PreUpdate) - 영속성 관리(JPA/Hibernate)
@Entity
@Table(name = "reservation")
public class Reservation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 64)
private String token;
@Column(name = "customer_name", nullable = false, length = 200)
private String customerName;
@Enumerated(EnumType.STRING)
private Status status;
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;
@Temporal(TemporalType.TIMESTAMP)
private Date updatedAt;
@PrePersist
protected void onCreate() {
createdAt = new Date();
updatedAt = new Date();
status = Status.ACTIVE;
}
@PreUpdate
protected void onUpdate() {
updatedAt = new Date();
}
}
💡 Entity 설계 시 중요한 점
- Enum은
STRING으로 매핑 → 순서 변경 안전- 날짜는
TemporalType.TIMESTAMP사용- DB 제약조건(
nullable,unique,length)을 코드 레벨에서 표현
6️⃣ DAO / Repository: DB 접근 계층
DAO(Data Access Object) 또는 Respository는 Entity를 실제 DB에 저장하고 조회하는 계층입니다.
🛠️ 역할
- CRUD 수행
- 복잡한 Query 처리
- 트랜잭션 범위 내에서 DB 접근
- Service에 노출하지 않고 Entity만 주고받음
@Repository
public class ReservationDao extends DaoBase {
public Long insert(Reservation reservation) {
return (Long) session().save(reservation);
}
public Reservation select(Long id) {
return session().get(Reservation.class, id);
}
}
✅ DAO/Repository가 있으면 Service는 DB 구현 세부사항을 몰라도 됨
✅ 유지보수, 테스트, DB 변경 유연성이 좋아짐
7️⃣ 전체 요청/응답 흐름 예시
- 클라이언트가 예약 요청 JSON 전송
- Controller에서 ReservationDto로 바인딩
- Service에서 DTO 검증, Entity 변환
- DAO가 DB에 Entity 저장
- Service에서 Entity → Response DTO 변환
- Controller가 클라이언트에게 Response DTO 반환